# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import base64
import enum
import hashlib
import hmac
import logging
import operator
import time
import urllib.parse

import requests
import requests.auth

from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.exchange.util.rate_limit import RateLimiter

GDAX_API_HOST = 'api.pro.coinbase.com'
GDAX_SANDBOX_API_HOST = 'api-public.sandbox.pro.coinbase.com'


class GdaxOrderSide(enum.Enum):
  BUY = 'buy'
  SELL = 'sell'


class GdaxOrderType(enum.Enum):
  LIMIT = 'limit'
  MARKET = 'market'


class GdaxTimeInForce(enum.Enum):
  GTC = 'GTC'
  GTT = 'GTT'
  IOC = 'IOC'
  FOK = 'FOK'


class GdaxStpFlag(enum.Enum):
  DECREASE_AND_CANCEL = 'dc'
  CANCEL_OLDEST = 'co'
  CANCEL_NEWEST = 'cn'
  CANCEL_BOTH = 'cb'


class GdaxOrderStatus(enum.Enum):
  RECEIVED = 'received'
  OPEN = 'open'
  DONE = 'done'
  DONE_CANCELED = 'done_canceled'
  DONE_FILLED = 'done_filled'
  REJECTED = 'rejected'
  PENDING = 'pending'
  ALL = 'all'


class GdaxFillType(enum.Enum):
  MAKER_FILL_TYPE = 'M'
  TAKER_FILL_TYPE = 'T'


class GdaxAuth(requests.auth.AuthBase):
  def __init__(self, key_file):
    self._key = AuthKey.from_file(key_file)
    self._key.passphrase = self._key.get_value('passphrase')

  def gen_auth(self, timestamp, method, path_url, body=None):
    """Auth method reused for both rest and ws"""
    if body is not None:
      body = body.decode()
    else:
      body = ''
    message = timestamp + method + path_url + body
    hmac_key = base64.b64decode(self._key.secret_key)
    signature = hmac.new(hmac_key, message.encode(), hashlib.sha256)
    signature_b64 = base64.b64encode(signature.digest()).decode()
    return {
        'CB-ACCESS-SIGN': signature_b64,
        'CB-ACCESS-TIMESTAMP': timestamp,
        'CB-ACCESS-KEY': self._key.api_key,
        'CB-ACCESS-PASSPHRASE': self._key.passphrase,
        'Content-Type': 'application/json'
    }

  def __call__(self, req: requests.PreparedRequest):
    timestamp = str(time.time())
    sign_header = self.gen_auth(timestamp, req.method, req.path_url, req.body)
    req.headers.update(sign_header)
    return req


class GdaxSyncQueryer(object):
  def __init__(self, key_file, *, api_host=None, disable_ssl=False, timeout=10):
    self._timeout = timeout
    self._auth = GdaxAuth(key_file) if key_file else None
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or GDAX_API_HOST)
    self.rate_limit = RateLimiter(100, 10)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, method, path, params):
    assert method in ('GET', 'POST', 'DELETE'), method
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    data = None
    if method != 'GET':
      data = params
      params = None

    try:
      response = requests.request(method,
                                  url,
                                  params=params,
                                  json=data,
                                  auth=self._auth,
                                  timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException: %s')
      raise
    except ValueError:
      self._logger.exception('Json decode: %s', response.content)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(method, path, params)


class GdaxAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=4,
  ):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = GdaxAuth(key_file) if key_file else None
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or GDAX_API_HOST)

  def query(self, method, path, params=None):
    assert method in ('GET', 'POST', 'DELETE'), method
    data = None
    if method != 'GET':
      data = params
      params = None

    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(method=method,
                                     url=url,
                                     params=params,
                                     json=data,
                                     auth=self._auth,
                                     timeout=self._timeout)
    return future


class GdaxNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      use_async=False,
  ):
    if use_async:
      self._auth_queryer = GdaxAsyncQueryer(key_file,
                                            api_host=api_host,
                                            disable_ssl=disable_ssl,
                                            timeout=timeout,
                                            max_connections=max_connections)
    else:
      self._auth_queryer = GdaxSyncQueryer(key_file, api_host=api_host, timeout=timeout)

  def _query(self, method, path, params=None):
    if params is not None:
      params = {key: value for key, value in params.items() if value is not None}
    params = params or None
    return self._auth_queryer.query(method, path, params=params)

  # List Accounts.
  def query_accounts(self):
    method = 'GET'
    path = 'accounts'
    update = self._query(method, path)
    return update

  # Get an Account
  def query_single_account(self, account_id):
    method = 'GET'
    path = 'accounts/%s' % account_id
    update = self._query(method, path)
    return update

  # Get Account History
  def query_account_history(self, account_id):
    method = 'GET'
    path = 'accounts/%s/ledger' % account_id
    update = self._query(method, path)
    return update

  # Get Holds
  def query_holds(self, account_id):
    method = 'GET'
    path = 'accounts/%s/holds' % account_id
    update = self._query(method, path)
    return update

  # Place a New Order
  def place_order(self,
                  *,
                  product_id: str,
                  client_oid: str,
                  order_type: GdaxOrderType,
                  side: GdaxOrderSide,
                  price,
                  size,
                  time_in_force: GdaxTimeInForce,
                  cancel_after=None,
                  post_only: bool,
                  stp: GdaxStpFlag,
                  stop=None,
                  stop_price=None):
    assert stop is None and stop_price is None, 'stop not supported by this client'
    method = 'POST'
    path = 'orders'
    params = {
        'product_id': product_id,
        'client_oid': client_oid,
        'side': side.value,
        'price': price,
        'size': size,
        'time_in_force': time_in_force.value,
        'cancel_after': cancel_after,
        'type': order_type.value,
        'post_only': post_only,
        'stp': stp.value,
    }
    update = self._query(method, path, params)
    return update

  # Cancel a previously placed order.
  def cancel_order(self, order_id):
    method = 'DELETE'
    path = 'orders/%s' % order_id
    update = self._query(method, path)
    return update

  # With best effort, cancel all open orders.
  # The response is a list of ids of the canceled orders.
  def cancel_all_orders(self, product_id=None):
    method = 'DELETE'
    path = 'orders'
    params = {
        'product_id': product_id,
    }
    update = self._query(method, path, params)
    return update

  # List recent orders.
  def query_orders(self, status: GdaxOrderStatus, product_id=None):
    method = 'GET'
    path = 'orders'
    params = {
        'status': status.value,
        'product_id': product_id,
    }
    update = self._query(method, path, params)
    return update

  # Get a single order by order id.
  def query_single_order(self, order_id):
    method = 'GET'
    path = 'orders/%s' % order_id
    update = self._query(method, path)
    return update

  # Get a list of recent fills.
  def query_fills(self, order_id=None, product_id=None):
    assert operator.xor(bool(order_id), bool(product_id))
    method = 'GET'
    path = 'fills'
    params = {
        'order_id': order_id,
        'product_id': product_id,
    }
    update = self._query(method, path, params)
    return update

  # Get accounts withdraw history
  def query_withdraw_history(self):
    method = 'GET'
    path = 'transfers'
    params = {'type': 'internal_withdraw'}
    update = self._query(method, path, params)
    msg = update.msg
    params = {'type': 'withdraw'}
    update = self._query(method, path, params)
    update.msg += msg
    return update
  
  # Get accounts withdraw history
  def query_deposit_history(self):
    method = 'GET'
    path = 'transfers'
    params = {'type': 'internal_deposit'}
    update = self._query(method, path, params)
    msg = update.msg
    params = {'type': 'deposit'}
    update = self._query(method, path, params)
    update.msg += msg
    return update
